import asyncio
import os

import arduinoserial
import termios
import serial_asyncio
from common.third_party.timeout_decorator import timeout as timeout_decorator
from services.ad936x.api import Ad936xApi
from services.ad936x.schemas.configs import DoubleFrequency
from services.control.api import ControlApi
from services.ocb.api import OcbApi
from services.relays_module.schemas.configs import VcoConfig, RmConfig, RmTelemetry
from common.third_party.save_config import load_config
from settings import settings
from async_timeout import timeout



class RelaysModule:
    __slots__ = ['rm_configs', 'rm_telemetries']

    DEVICES = [{'port_name': '/dev/ttyUL4', 'baudrate': 9600, 'timeout': 1},
               {'port_name': '/dev/ttyUL5', 'baudrate': 9600, 'timeout': 1}]

    AD_API = Ad936xApi()
    CTRL_API = ControlApi()
    OCB_API = OcbApi()

    def __init__(self) -> None:
        self.rm_configs: list[RmConfig] = [
            load_config(f'rm/rm_config_{i}', RmConfig, RmConfig())
            for i in range(settings.SERVICES_RELAYS_NUMBER)
        ]
        for rm_config in self.rm_configs:
            rm_config.commit = False

        self.rm_telemetries = [RmTelemetry() for _ in range(settings.SERVICES_RELAYS_NUMBER)]

    @staticmethod
    def mbcrc(device_commands) -> int:
        crc: int = 0xFFFF
        for i in range(len(device_commands)):
            crc ^= device_commands[i]
            for j in range(8):
                if (crc & 1) == 1:
                    crc >>= 1
                    crc ^= 0xA001
                else:
                    crc >>= 1
        return crc

    async def rm_loop(self) -> None:
        while True:
            try:
                ocb_current_28v = await self.OCB_API.get_all_ocb_current_28v()

                for index, rm_config in enumerate(self.rm_configs):
                    if not await self.CTRL_API.get_control_state(f'link_{index}_state'):
                        self.rm_telemetries[index] = RmTelemetry()
                        continue

                    vco_config, device_commands = await self.create_device_commands(index, rm_config)
                    rm_config.read_back = (rm_config.read_back + 1) & 0xff
                    if rm_config.commit:
                        device_commands.append(rm_config.read_back)
                        device_commands.append(self.mbcrc(device_commands))

                    if index == 0:
                        await self.AD_API.set_center_frequency(
                            0,
                            DoubleFrequency(rx_frequency=vco_config.rx_center_frequency,
                                            tx_frequency=vco_config.tx_center_frequency)
                        )

                    rm_config.actual_rx_frequency = vco_config.actual_rx_frequency
                    rm_config.actual_tx_frequency = vco_config.actual_tx_frequency
                    self.rm_configs[index] = rm_config

                    if not settings.DEBUG:
                        @timeout_decorator(5.0)
                        def aserial_port():
                            return arduinoserial.SerialPort(
                                self.DEVICES[index].get('port_name'),
                                self.DEVICES[index].get('baudrate')
                            )
                        arduino = aserial_port()

                        @timeout_decorator(5.0)
                        def tcflush():
                            termios.tcflush(arduino.fd, 0)
                        tcflush()

                        await self.write_commands(self.DEVICES[index], device_commands)

                        if rm_config.commit:
                            rm_config.commit = False
                        else:
                            data, rm_message = await asyncio.wait_for(self.read_telemetry(self.DEVICES[index]), 15.0)
                            if data is not None:
                                self.rm_telemetries[index] = \
                                    RmTelemetry(temperature=self.decode_temp(self.decode_rm(data[0])),
                                                voltage_28v=self.volt_coef(self.decode_rm(data[1]), 80.6, 4.7),
                                                voltage_8v=self.volt_coef(self.decode_rm(data[2]), 80.6, 10),
                                                voltage_5v=self.volt_coef(self.decode_rm(data[3]), 80.6, 27.4),
                                                preamp_current=self.decode_rm(data[4]),
                                                pa_current=self.decode_rm(data[5]),
                                                rf_direct_detector_out=self.decode_rm(data[6]),
                                                rf_rev_detector_out=data[7],
                                                rx_synth_lock=self.which_rx_tx(data[8]),
                                                tx_synth_lock=self.which_rx_tx(data[9]),
                                                raw_message=str(rm_message))
                            else:
                                self.rm_telemetries[index] = RmTelemetry()

                        self.rm_telemetries[index].current_rm_module = \
                            ocb_current_28v.get(f'link_{index}_ocb')

                        os.close(arduino.fd)
            except Exception as e:
                #print(e)
                pass

            await asyncio.sleep(3.8)

    @staticmethod
    def decode_temp(value) -> float:
        return -2.3654 * value ** 2 - 78.154 * value + 153.857

    @staticmethod
    def volt_coef(u2, r1, r2) -> float:
        return (r1 + r2) * u2 / r2

    @staticmethod
    def which_rx_tx(value) -> str:
        match value:
            case 1:
                return 'TLO'
            case 2:
                return 'THI'
            case 4:
                return 'LOCK'
            case 8:
                return 'ALCKO'
            case 16:
                return 'ALCHI'
            case 32:
                return 'UNLOCK'

    @staticmethod
    def decode_rm(value) -> float:
        return int(value) * 3.27 / (2 ** 12 - 1)

    async def create_device_commands(self, index: int, rm_config: RmConfig) -> tuple[VcoConfig, list]:
        device_commands = [0xAA if index == 0 else 0xBB,
                           0x20 if rm_config.commit else 0x15]

        vco_config = VcoConfig()
        if index == 0 or (await self.CTRL_API.get_control_state(f'link_0_state')) is False:
            vco_config = self.calculate_method_for_1st_link(rm_config.rx_frequency, rm_config.tx_frequency)
        elif index == 1 and (await self.CTRL_API.get_control_state(f'link_0_state')) and \
                (await self.CTRL_API.get_control_state(f'link_1_state')):
            vco_config = await self.calculate_method_for_2d_link(rm_config.rx_frequency, rm_config.tx_frequency)

        if rm_config.commit:
            device_commands.extend([int(round(abs(rm_config.out_amp_neg / (2.5 / 255)))),
                                    int(round(abs(rm_config.pre_amp_neg / (2.5 / 255)))),
                                    1 if rm_config.ten_v else 0,
                                    1 if rm_config.pre_amp else 0])

            reg_rx = self.create_register_map(vco_config.r_div, vco_config.n_div_rx_round, vco_config.f_pfd)
            device_commands.extend(reg_rx)

            reg_tx = self.create_register_map(vco_config.r_div, vco_config.n_div_tx_round, vco_config.f_pfd)
            device_commands.extend(reg_tx)

        return vco_config, device_commands

    async def calculate_method_for_2d_link(self, rx_frequency, tx_frequency) -> VcoConfig:
        rx_center_frequency = (await self.AD_API.get_config(index=0)).rx_frequency
        tx_center_frequency = (await self.AD_API.get_config(index=0)).tx_frequency

        f_ref = 40_000_000  # Reference frequency
        f_step = 40_000_000  # Frequency tuning step

        lo_rx = rx_frequency - rx_center_frequency  # LO Frequency value for LTC6946
        lo_tx = tx_frequency - tx_center_frequency  # LO Frequency value for LTC6946

        r_div = round(f_ref / f_step)  # Division ratio in R divider
        f_pfd = round(f_ref / r_div)  # Phase/Frequency Detector Frequency, int or float

        n_div_rx = lo_rx / f_pfd  # Division ratio in N divider
        n_div_tx = lo_tx / f_pfd  # Division ratio in N divider

        n_div_rx_round = int(n_div_rx)  # Rounded division ratio in N divider. Values of registers h05, h06 in LTC6946
        n_div_tx_round = int(n_div_tx)  # Rounded division ratio in N divider. Values of registers h05, h06 in LTC6946

        actual_rx_frequency = round((f_ref * n_div_rx_round) / r_div) + rx_center_frequency
        actual_tx_frequency = round((f_ref * n_div_tx_round) / r_div) + tx_center_frequency

        return VcoConfig(actual_rx_frequency=actual_rx_frequency,
                         actual_tx_frequency=actual_tx_frequency,
                         rx_center_frequency=rx_center_frequency,
                         tx_center_frequency=tx_center_frequency,
                         r_div=r_div,
                         n_div_rx_round=n_div_rx_round,
                         n_div_tx_round=n_div_tx_round,
                         f_pfd=f_pfd)

    @staticmethod
    def calculate_method_for_1st_link(rx_frequency, tx_frequency) -> VcoConfig:
        f_ref = 40_000_000  # Reference frequency
        f_step = 40_000_000  # Frequency tuning step

        if_frequency_rx_central = 1_750_000_000  # Central frequency SAW filter
        if_frequency_tx_central = 1_330_000_000  # Central frequency SAW filter

        lo_rx = rx_frequency - if_frequency_rx_central  # LO Frequency value for LTC6946
        lo_tx = tx_frequency - if_frequency_tx_central  # LO Frequency value for LTC6946

        r_div = round(f_ref / f_step)  # Division ratio in R divider
        f_pfd = round(f_ref / r_div)  # Phase/Frequency Detector Frequency, int or float

        n_div_rx = lo_rx / f_pfd  # Division ratio in N divider
        n_div_tx = lo_tx / f_pfd  # Division ratio in N divider

        n_div_rx_round = round(n_div_rx)  # Rounded division ratio in N divider. Values of registers h05, h06 in LTC6946
        n_div_tx_round = round(n_div_tx)  # Rounded division ratio in N divider. Values of registers h05, h06 in LTC6946

        n_div_rx_fractional_round = -round((n_div_rx_round - n_div_rx),
                                           5)  # Difference between rounded and original value
        n_div_tx_fractional_round = -round((n_div_tx_round - n_div_tx),
                                           5)  # Difference between rounded and original value

        # Frequency offset relative to Central frequency SAW filter
        if_frequency_rx_offset = f_step * n_div_rx_fractional_round
        # Frequency offset relative to Central frequency SAW filter
        if_frequency_tx_offset = f_step * n_div_tx_fractional_round

        rx_center_frequency = if_frequency_rx_central + if_frequency_rx_offset
        tx_center_frequency = if_frequency_tx_central + if_frequency_tx_offset

        actual_rx_frequency = round((f_ref * n_div_rx_round) / r_div) + rx_center_frequency
        actual_tx_frequency = round((f_ref * n_div_tx_round) / r_div) + tx_center_frequency

        return VcoConfig(actual_rx_frequency=actual_rx_frequency,
                         actual_tx_frequency=actual_tx_frequency,
                         rx_center_frequency=rx_center_frequency,
                         tx_center_frequency=tx_center_frequency,
                         r_div=r_div,
                         n_div_rx_round=n_div_rx_round,
                         n_div_tx_round=n_div_tx_round,
                         f_pfd=f_pfd)

    @staticmethod
    def create_register_map(r_div: int = 1, n_div_round: int = 0, f_pfd: int = 0) -> list[int]:
        lk_win = 3 if f_pfd <= 550_000 else 2 if f_pfd <= 1_700_000 else 1 if f_pfd <= 5_000_000 else 0

        h00 = 0x00
        h01 = 0x04
        h02 = 0x0C
        h03 = 0b0100_0000 | (int(format(r_div, '#012b')[2:4], 2) & 0x3)
        h04 = int(format(r_div, '#012b')[4:], 2) & 0xFF
        h05 = int(format(n_div_round, '#018b')[2:10], 2) & 0xFF
        h06 = int(format(n_div_round, '#018b')[10:], 2) & 0xFF
        h07 = 0x83
        h08 = 0xB9
        h09 = 0b0000_1000 | ((lk_win & 0x3) << 6)
        h0a = 0x00

        return [0x00, h00, h01, h02, h03, h04, h05, h06, h07, h08, h09, h0a]

    @staticmethod
    async def read_telemetry(device: dict) -> tuple[list[int] | None, bytes]:
        def parse(data: bytes):
            def crc(data: str):
                crc = 0xFFFF

                for ch in data:
                    crc ^= ord(ch)
                    for _ in range(0, 8):
                        bcarry = crc & 0x0001
                        crc >>= 1
                        if bcarry:
                            crc ^= 0xA001

                return crc

            data = data.decode('ascii')
            begin, *msgs, end = data.split('#')

            target_crc = int(end.removeprefix('END'), base=16)
            if begin not in ['7G1', '7G2']:
                return None

            msg = data.removeprefix(begin).removesuffix(end)
            msg = f'{begin}{msg}END'
            if crc(msg) != target_crc:
                return None

            return [int(word) for word in msgs]

        try:
            reader, writer = await asyncio.wait_for(serial_asyncio.open_serial_connection(
                loop=asyncio.get_event_loop(),
                url=device.get('port_name'), baudrate=device.get('baudrate'),
                timeout=5, write_timeout=5
            ), 3.0)
            try:
                async with timeout(10.0):
                    byte_array = await reader.readline()
            except Exception as e:
                writer.close()
                return None, None

            data = parse(byte_array)
            writer.close()
            return data, byte_array
        except Exception as e:
            #print(e)
            return None, None

    @staticmethod
    async def write_commands(device: dict, device_commands: list[int]) -> None:
        try:
            reader, writer = await asyncio.wait_for(serial_asyncio.open_serial_connection(
                loop=asyncio.get_event_loop(),
                url=device.get('port_name'), baudrate=device.get('baudrate'),
                timeout=1
            ), 3.0)
            bytes_array = b''
            for command in device_commands:
                if len(str(command)) > 1:
                    bytes_array += command.to_bytes(len(str(command)), 'little').split(b'\x00')[0]
                else:
                    bytes_array += command.to_bytes(1, 'little')

            @timeout_decorator(5.0)
            def write_command():
                writer.write(bytes_array)
            write_command()
            writer.close()
        except Exception as e:
            #print(e)
            pass
